Искусство схемотехники. Том 3 [Изд.4-е] - Пауль Хоровиц
Шрифт:
Интервал:
Закладка:
Упражнение 11.12. Но почему?
Можно, конечно, выключать прерывания, генерировать сигнал, а затем снова включать прерывания. Это безобразное решение, поскольку в самый важный процесс регулярных выборок вносятся нежелательные задержки. Потом мы нашли лучший способ: вывод на экран одной точки выполнять, как вспомогательную задачу обработчиком прерываний. Обработчик срабатывает каждые 100 мкс, так что полное 256-точечное изображение будет выводиться 40 раз в секунду. При этом, поскольку прерывания возникают и в том случае, когда главная программа находится в состоянии ожидания (сигнала ПУСК), изображение на экране не будет гаснуть. Наконец, такой способ содержит в себе чудесную глюковину: ведь запустив АЦП, приходится выжидать 10 мкс перед тем, как снимать с него результат преобразования; этого времени как раз хватит, чтобы послать в ЦАП пару X, Y. Другими словами, регенерация дисплея в обработчике прерываний абсолютно не требует процессорного времени!
Главная программа: инициализация. Хватит нам ходить вокруг до около. Давайте рассмотрим подетальнее задачи, выполняемые программой. Сначала взгляните на главную программу, изображенную на рис. 11.19 в виде несколько необычной структурной схемы.
Рис. 11.19. Структурная схема главной программы.
Приведенная диаграмма весьма близко соответствует собственно программе на языке ассемблера (программа 11.3).
Текст программы начинается с определений адресов ОЗУ (включая вектор прерывания, область переменных и массивы), а также адресов (и бит) портов. В дальнейшем эти определения будут использоваться в качестве операндов команд обращения к памяти и портам, причем ассемблер подставит на их место фактические адреса. Хотя результат не зависит от того, пользуетесь ли вы определениями или непосредственно адресами, всегда следует использовать определения, так как в этом случае программа становится более наглядной и, кроме того, облегчается изменение назначения портов и битов в последующих модификациях. Адреса портов соответствуют нашей схеме и включают внутренние регистры периферийных устройств, адресуемые с помощью младших бит адреса или путем двухбайтовых пересылок.
Из текста программы также видно, как мы будем использовать регистры МП 68008. При каждом прерывании мы извлекаем данные из АЦП, добавляем их к текущему содержимому канала и проверяем, не дошли ли мы до конца канала или развертки. Можно было хранить содержимое указателей и счетчиков в памяти (так и пришлось бы поступать при использовании менее совершенного процессора типа 8086), но зарезервировав достаточное число регистров для нужд обработчика прерываний, мы существенно повышаем эффективность режима прерываний. Поэтому мы выделили регистры данных для текущего содержимого канала (D7), обратного счетчика периодов дескретизации (внутри канала) (D6) и обратного счетчика каналов внутри развертки (D5), смещения в массиве DISPLAY (D4), а также регистр для временных данных (D3). Далее, мы зарезервировали адресные регистры для трех массивов (NORM, А6; DATA, А5; DISPLAY, А4) и для наиболее используемых портов (ADC0, A3; СIO [параллельный порт], А2). Главная программа берет на себя обязательство не использовать эти регистры при включенных прерываниях.
Вам может показаться странным, что мы резервируем адресные регистры (со всеми их автоинкрементными возможностями, ориентированными на работу с массивами) для адресации отдельных фиксированных портов, когда вполне можно было обойтись абсолютной адресацией. Причина заключается в быстродействии. Команда с абсолютной адресацией
MOVE.B ADC0, D0
где ADC0 представляет длинный абсолютный адрес (в нашем случае $80000), требует 28 тактов (3,5 мкс в нашем процессоре), в то время как команда
MOVE.B (A3), D0
использующая косвенную адресацию через A3, выполняется всего за 12 тактов. Эта разница обусловлена исключительно процессами на магистрали, где для пересылки каждого байта требуются (в МП 68008) 4 такта. В процессе выполнения первой команды ЦП извлекает из памяти двухбайтовый код операции, четырехбайтовое расширение (длинного) адреса и, наконец, запрошенный байт данных, т. е. всего 7 байт, на что расходуется 28 тактов. Вторая команда требует извлечения двухбайтового кода операции и запрошенного байта данных, т. е. всего 3 байт (12 тактов). Вообще системы с узкими шинами (вроде нашего МП 68008, у которого внутренняя 32-разрядная архитектура должна себя чувствовать как в смирительной рубашке, общаясь с внешним миром через 8-разрядную шину) особенно неэффективны в условиях интенсивных передач данных.
Наконец, началась программа! Первые 8 байт ПЗУ хранят важнейший стартовый вектор: указатель стека и входную точку программы. Входная точка находится в «истинном» ПЗУ (по адресу $40008), поэтому мы можем немедленно очистить бит BOOT, что приводит к замещению временного образа ПЗУ, используемого при начальной загрузке, оперативной памятью. Теперь мы можем загружать векторы прерываний в начало ОЗУ, в конкретные ячейки, определяемые архитектурой МП 68008 (вся область векторов приведена в табл. 11.5): $68 (INT2), $74 (INT5) и $7С (NMI = INT7). Мы использовали только INT5 (от 100 мкс — таймера в микросхеме параллельного порта); в этот вектор мы загружаем адрес нашего обработчика прерываний. В зависимости от конкретного состояния прибора (ожидание пуска или внешнего сигнала запуска, начало новой развертки, процесс развертки) обработчик прерываний должен выполнять различные функции; поэтому мы написали один грандиозный обработчик со многими точками входа, соответствующими его функциям. На данном этапе мы еще не готовы принимать данные, поэтому в вектор INT5 мы загружаем входную точку idle__int (прерывание простоя). Очень полезно загрузить на всякий случай все неиспользуемые векторы прерываний адресом bad__int (ложное прерывание) (вдруг произойдет деление на нуль, ложное прерывание и т. д.); мы загружаем в них адрес программы, которая зажигает ЭЛД определенным образом (далее будет видно, каким именно).
Теперь наступает утомительный, но существенный этап инициализации портов. БИС периферийных устройств, как, например, 8536, обладают изумительной гибкостью, но за нее приходится платить тщательным планированием. Вы должны продумать, какие управляющие байты следует послать, в какие регистры и в каком порядке, чтобы получить требуемый результат. Для простых параллельных портов в процессе планирования следует выбрать направление, полярность, режим и прерывания, а для таймеров — основание счета, каскадирование, режим запуска, прерывания и проч. В программе 11.3 приведен полный текст инициализации параллельного порта/таймера. Разрешаются параллельные порты А, В и С, причем биты 4–6 порта В назначаются выходными, а остальные - входными (см. рис. 11.15). Таймер-0 настраивается на деление его тактовой частоты 4 МГц на 400 и на непрерывный перезапуск с генерацией прерывания (по INT5) каждые 100 мкс. Заметьте, что все установочные входы мы сделали инверсными, поэтому при замыкании контакта (на который изначально подано +5 В) на землю с него считывается 1, а не 0. На входе, к которому подключена кнопка СТОП, мы использовали опцию «запоминания 1», так что мгновенное нажатие фиксируется, а отрабатывается оно только в конце развертки.
Наконец, мы очищаем массивы в ОЗУ (отметьте использование подпрограммы), инициализируем регистры, разрешаем прерывания и переходим на выполнение «главного» цикла.
Главная программа: главный цикл. Завершив инициализацию, мы входим в бесконечный главный цикл main__loop. Фактически он состоит из двух циклов: цикла ожидания нажатия кнопки ПУСК и цикла непрерывного обновления памяти изображения, на фоне которого осуществляется сбор данных в режиме прерываний. Программа обработки прерываний, завершив последнюю развертку, устанавливает программный «флаг останова» stop__flag, который непрерывно проверяется вторым главным циклом. Обнаружив установленный флаг, главная программа возвращается в первый цикл ожидания нового пуска. Давайте сопоставим структурную схему и программные строки.
Главный цикл (рис. 11.19) начинается с установки на ЭЛД состояния «ожидание». Затем программа ждет нажатия кнопки ПУСК, т. е. ее перехода из разомкнутого в замкнутое состояние. Это сложнее, чем кажется, потому что кнопка не содержит цепей подавления дребезга, в результате чего вы имеете несколько десятков близко расположенных перепадов между уровнями «замкнуто» и «разомкнуто», возникающих на протяжении, возможно, 25 мс. Этого времени может хватить на завершение самого короткого цикла измерений (если вы выбрали 1 развертку и интервал дискретизации 100 мкс), после чего измерения будут ошибочно продолжены, поскольку контакт кнопки все еще колеблется между состояниями «разомкнуто» и «замкнуто». Поэтому мы написали простенькую программу подавления дребезга, которая фиксирует, что кнопка была непрерывно разомкнута в течение приблизительно 50 мс (тем временем многократно выполняется подпрограмма обновления update), а затем переходит в состояние «замкнуто». Наконец мы получили приказ на выступление!